/*
 * Sales Data Controller Project.
 * Copyright (C) 2011 FedICT.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version
 * 3.0 as published by the Free Software Foundation.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, see 
 * http://www.gnu.org/licenses/.
 */

package be.fedict.sdc.vsc.tlv;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class TLVMarshaller<T> {

	private static final Log LOG = LogFactory.getLog(TLVMarshaller.class);

	private final TLVMetadata tlvMetadata;

	private final Map<Class<?>, TLVCodec<?>> tlvCodecs;

	public TLVMarshaller(TLVMetadata tlvMetadata,
			Map<Class<?>, TLVCodec<?>> tlvCodecs) {
		this.tlvMetadata = tlvMetadata;
		this.tlvCodecs = tlvCodecs;
	}

	public byte[] marshall(T tlvObject) throws IOException {
		if (null == tlvObject) {
			return null;
		}
		if (false == this.tlvMetadata.canHandle(tlvObject.getClass())) {
			throw new IOException("invalid class type: "
					+ tlvObject.getClass().getName());
		}
		ByteArrayOutputStream output = new ByteArrayOutputStream();
		Set<Field> fields = this.tlvMetadata.getFields();
		for (Field field : fields) {
			int tag = this.tlvMetadata.getTag(field);
			output.write(tag);
			Class<?> type = field.getType();
			TLVCodec<Object> tlvCodec = (TLVCodec<Object>) this.tlvCodecs
					.get(type);
			if (null == tlvCodec) {
				throw new IllegalArgumentException(
						"unsupported TLV field type: " + type.getName());
			}
			Object value;
			try {
				field.setAccessible(true);
				value = field.get(tlvObject);
			} catch (Exception e) {
				throw new IOException("cannot read field: " + field.getName());
			}
			byte[] encodedValue = tlvCodec.encode(value);

			int length = encodedValue.length;
			int lengthFieldLength = 0;
			while (length > 0) {
				lengthFieldLength++;
				length >>= 7;
			}
			while (lengthFieldLength > 0) {
				byte lengthByte = (byte) (encodedValue.length >> (7 * (lengthFieldLength - 1)));
				lengthByte &= 0x7f;
				lengthFieldLength--;
				if (lengthFieldLength > 0) {
					lengthByte |= 0x80;
				}
				LOG.debug("length byte: "
						+ Integer.toHexString((byte) lengthByte & 0xff));
				output.write(lengthByte);
			}

			output.write(encodedValue);
		}
		return output.toByteArray();
	}
}
